/*
 * Jamus
 * Copyright (C) 2010 Jamus Team
 *
 * Jamus is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Jamus is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Jamus.  If not, see <http://www.gnu.org/licenses/>.
 */

#define BOOST_TEST_DYN_LINK
#define BOOST_TEST_MODULE stub back-end test

#include <boost/test/unit_test.hpp>
#include <boost/assign/list_of.hpp>
#include <boost/thread.hpp>
#include <string>

#include "Plugin.h"
#include "../../NoCurrentTrackException.h"
#include "../../InvalidPlaybackStateException.h"

using namespace std;
using backend::stub::Plugin;

namespace {
	const Track TRACK1 = Track("filename1", boost::posix_time::seconds(10));
	const Track TRACK2 = Track("filename2", boost::posix_time::seconds(20));

	const Plugin::TrackList TRACK_LIST = boost::assign::list_of(TRACK1)(TRACK2);
	const Plugin::Playlist PLAYLIST = boost::assign::list_of(TRACK1)(TRACK2);
	const int VOLUME = 50;
	const Plugin::PlaybackStatus PLAYBACK_STATUS = Plugin::STOP;

	// Context for each test case. Populate the plugin with stub tracks
	class Fixture : public Plugin {
	public:
		Fixture()
		: Plugin(TRACK_LIST, PLAYLIST, PLAYBACK_STATUS, VOLUME),
		  runThread(boost::bind(&Fixture::run, this))
		{
			connectPlaybackStatusChanged(boost::bind(&Fixture::onPlaybackStatusChanged, this));
			connectVolumeChanged(boost::bind(&Fixture::onVolumeChanged, this));
			connectPlaylistChanged(boost::bind(&Fixture::onPlaylistChanged, this));
		}

		~Fixture()
		{
			cancel();
			runThread.join();
		}

		// Returns true iff the specified signal was scheduled to fire or already fired.
		// If so, removes it from the schedule and list of fired signals.
		// Only white-box check if the signal was scheduled, since we cannot easily tell when the slot will be called and theoretically might have to wait arbitrarily long
		bool signalScheduled(boost::signals2::signal<void()>& sig)
		{
			boost::lock_guard<boost::mutex> lock(mutex);

			bool wasScheduled = false;

			if(signalsFired.erase(&sig))
				wasScheduled = true;

			if(signalsToFire.erase(&sig))
				wasScheduled = true;

			return wasScheduled;
		}

		void onPlaybackStatusChanged() { signalsFired.insert(&sigPlaybackStatusChanged); }
		void onVolumeChanged() { signalsFired.insert(&sigVolumeChanged); }
		void onPlaylistChanged() { signalsFired.insert(&sigPlaylistChanged); }

	private:
		boost::thread runThread;
		set<boost::signals2::signal<void()>*> signalsFired;
	};
}

// Verify initial state is as documented
BOOST_FIXTURE_TEST_CASE(initial_tracklist, Fixture)
{
	BOOST_CHECK(getTrackList() == TRACK_LIST);
}

BOOST_FIXTURE_TEST_CASE(initial_playlist, Fixture)
{
	BOOST_CHECK(getPlaylist() == PLAYLIST);
}

BOOST_FIXTURE_TEST_CASE(initial_current_track, Fixture)
{
	BOOST_CHECK_THROW(getCurrentTrack(), NoCurrentTrackException);
}

BOOST_FIXTURE_TEST_CASE(initial_playback_status, Fixture)
{
	BOOST_CHECK_EQUAL(getPlaybackStatus(), PLAYBACK_STATUS);
}

BOOST_FIXTURE_TEST_CASE(initial_volume, Fixture)
{
	BOOST_CHECK_EQUAL(getVolume(), VOLUME);
}

// Test method behavior
BOOST_FIXTURE_TEST_CASE(play_no_current_track_fails, Fixture)
{
	BOOST_CHECK_THROW(play(), NoCurrentTrackException);
}

BOOST_FIXTURE_TEST_CASE(play_track_plays_correct_track, Fixture)
{
	play(1);
	BOOST_CHECK_EQUAL(getPlaybackStatus(), PLAY);
	BOOST_CHECK_EQUAL(getCurrentTrack(), 1);
}

BOOST_FIXTURE_TEST_CASE(play_track_plays_from_beginning, Fixture)
{
	play(0);

	const boost::posix_time::ptime timeBefore = boost::posix_time::microsec_clock::local_time();
	play(0);
	const boost::posix_time::time_duration elapsed = getElapsed();
	const boost::posix_time::ptime timeAfter = boost::posix_time::microsec_clock::local_time();

	BOOST_CHECK(elapsed <= timeAfter - timeBefore);
}

BOOST_FIXTURE_TEST_CASE(play_if_playing_no_effect, Fixture)
{
	play(0);

	const boost::posix_time::time_duration elapsed = getElapsed();

	play();

	BOOST_CHECK_EQUAL(getPlaybackStatus(), PLAY);
	BOOST_CHECK_EQUAL(getCurrentTrack(), 0);
	BOOST_CHECK(getElapsed() >= elapsed); // Some time has passed...
	BOOST_CHECK_EQUAL(getVolume(), VOLUME);
	BOOST_CHECK(getTrackList() == TRACK_LIST);
	BOOST_CHECK(getPlaylist() == PLAYLIST);
}

BOOST_FIXTURE_TEST_CASE(play_resumes, Fixture)
{
	play(0);
	pause();
	play();
	BOOST_CHECK_EQUAL(getPlaybackStatus(), PLAY);
	BOOST_CHECK_EQUAL(getCurrentTrack(), 0);
}

BOOST_FIXTURE_TEST_CASE(pause_no_current_track_fails, Fixture)
{
	BOOST_CHECK_THROW(pause(), NoCurrentTrackException);
}

BOOST_FIXTURE_TEST_CASE(pause_not_playing_fails, Fixture)
{
	play(0);
	pause();
	BOOST_CHECK_THROW(pause(), InvalidPlaybackStateException);
	stop();
	BOOST_CHECK_THROW(pause(), InvalidPlaybackStateException);
}

BOOST_FIXTURE_TEST_CASE(pause_pauses, Fixture)
{
	play(0);
	pause();

	BOOST_CHECK_EQUAL(getPlaybackStatus(), PAUSE);
}

BOOST_FIXTURE_TEST_CASE(pause_retains_current_track, Fixture)
{
	play(0);
	pause();

	BOOST_CHECK_EQUAL(getCurrentTrack(), 0);
}

BOOST_FIXTURE_TEST_CASE(stop_stops, Fixture)
{
	play(0);
	stop();
	BOOST_CHECK_EQUAL(getPlaybackStatus(), STOP);
}

BOOST_FIXTURE_TEST_CASE(stop_retains_current_track, Fixture)
{
	play(0);
	stop();
	BOOST_CHECK_EQUAL(getCurrentTrack(), 0);
}

BOOST_FIXTURE_TEST_CASE(stop_no_effect_if_stopped, Fixture)
{
	stop();

	BOOST_CHECK_EQUAL(getPlaybackStatus(), STOP);
	BOOST_CHECK_THROW(getCurrentTrack(), NoCurrentTrackException);
	BOOST_CHECK_THROW(getElapsed(), NoCurrentTrackException);
	BOOST_CHECK_EQUAL(getVolume(), VOLUME);
	BOOST_CHECK(getTrackList() == TRACK_LIST);
	BOOST_CHECK(getPlaylist() == PLAYLIST);
}

BOOST_FIXTURE_TEST_CASE(playpause_no_current_track_fails, Fixture)
{
	BOOST_CHECK_THROW(playPause(), NoCurrentTrackException);
}

BOOST_FIXTURE_TEST_CASE(playpause_if_playing_pauses, Fixture)
{
	play(0);
	playPause();
	BOOST_CHECK_EQUAL(getPlaybackStatus(), PAUSE);
}

BOOST_FIXTURE_TEST_CASE(playpause_if_stopped_plays, Fixture)
{
	play(1);
	stop();
	playPause();
	BOOST_CHECK_EQUAL(getPlaybackStatus(), PLAY);
	BOOST_CHECK_EQUAL(getCurrentTrack(), 1);
}

BOOST_FIXTURE_TEST_CASE(playpause_if_paused_plays, Fixture)
{
	play(1);
	pause();
	playPause();
	BOOST_CHECK_EQUAL(getPlaybackStatus(), PLAY);
	BOOST_CHECK_EQUAL(getCurrentTrack(), 1);
}

BOOST_FIXTURE_TEST_CASE(nexttrack_no_current_track_fails, Fixture)
{
	BOOST_CHECK_THROW(nextTrack(), NoCurrentTrackException);
}

BOOST_FIXTURE_TEST_CASE(nexttrack_if_stopped_plays_next, Fixture)
{
	play(0);
	stop();
	nextTrack();
	BOOST_CHECK_EQUAL(getPlaybackStatus(), PLAY);
	BOOST_CHECK_EQUAL(getCurrentTrack(), 1);
}

BOOST_FIXTURE_TEST_CASE(nexttrack_if_paused_plays_next, Fixture)
{
	play(0);
	pause();
	nextTrack();
	BOOST_CHECK_EQUAL(getPlaybackStatus(), PLAY);
	BOOST_CHECK_EQUAL(getCurrentTrack(), 1);
}

BOOST_FIXTURE_TEST_CASE(nexttrack_if_playing_plays_next, Fixture)
{
	play(0);
	nextTrack();
	BOOST_CHECK_EQUAL(getPlaybackStatus(), PLAY);
	BOOST_CHECK_EQUAL(getCurrentTrack(), 1);
}

BOOST_FIXTURE_TEST_CASE(nexttrack_if_no_next_fails, Fixture)
{
	play(1);
	BOOST_CHECK_THROW(nextTrack(), range_error);
}

BOOST_FIXTURE_TEST_CASE(prevtrack_no_current_track_fails, Fixture)
{
	BOOST_CHECK_THROW(prevTrack(), NoCurrentTrackException);
}

BOOST_FIXTURE_TEST_CASE(prevtrack_if_stopped_plays_prev, Fixture)
{
	play(1);
	stop();
	prevTrack();
	BOOST_CHECK_EQUAL(getPlaybackStatus(), PLAY);
	BOOST_CHECK_EQUAL(getCurrentTrack(), 0);
}

BOOST_FIXTURE_TEST_CASE(prevtrack_if_paused_plays_prev, Fixture)
{
	play(1);
	pause();
	prevTrack();
	BOOST_CHECK_EQUAL(getPlaybackStatus(), PLAY);
	BOOST_CHECK_EQUAL(getCurrentTrack(), 0);
}

BOOST_FIXTURE_TEST_CASE(prevtrack_if_playing_plays_prev, Fixture)
{
	play(1);
	prevTrack();
	BOOST_CHECK_EQUAL(getPlaybackStatus(), PLAY);
	BOOST_CHECK_EQUAL(getCurrentTrack(), 0);
}

BOOST_FIXTURE_TEST_CASE(prevtrack_if_no_prev_fails, Fixture)
{
	play(0);
	BOOST_CHECK_THROW(prevTrack(), range_error);
}

BOOST_FIXTURE_TEST_CASE(getcurrenttrack_returns_current_track, Fixture)
{
	play(1);
	BOOST_CHECK_EQUAL(getCurrentTrack(), 1);
}

BOOST_FIXTURE_TEST_CASE(getcurrenttrack_no_current_track_fails, Fixture)
{
	BOOST_CHECK_THROW(getCurrentTrack(), NoCurrentTrackException);
}

BOOST_FIXTURE_TEST_CASE(getcurrenttrack_retains_current_track_on_stop, Fixture)
{
	play(1);
	stop();
	BOOST_CHECK_EQUAL(getCurrentTrack(), 1);
}

BOOST_FIXTURE_TEST_CASE(getelapsed_no_current_track_fails, Fixture)
{
	BOOST_CHECK_THROW(getElapsed(), NoCurrentTrackException);
}

BOOST_FIXTURE_TEST_CASE(getelapsed_returns_elapsed, Fixture)
{
	play(0);
	boost::this_thread::sleep(boost::posix_time::milliseconds(200));
	BOOST_CHECK(getElapsed() >= boost::posix_time::milliseconds(200));
}

BOOST_FIXTURE_TEST_CASE(seek_no_current_track_fails, Fixture)
{
	BOOST_CHECK_THROW(seek(boost::posix_time::seconds(5)), NoCurrentTrackException);
}

BOOST_FIXTURE_TEST_CASE(seek_stopped_fails, Fixture)
{
	play(0);
	stop();
	BOOST_CHECK_THROW(seek(boost::posix_time::seconds(5)), InvalidPlaybackStateException);
}

BOOST_FIXTURE_TEST_CASE(seek_seeks, Fixture)
{
	play(0);
	seek(boost::posix_time::seconds(5));
	BOOST_CHECK(getElapsed() >= boost::posix_time::seconds(5));
}

// getVolume is already tested in "initial_volume"

BOOST_FIXTURE_TEST_CASE(setvolume_sets_volume, Fixture)
{
	setVolume(0);
	BOOST_CHECK_EQUAL(getVolume(), 0);
	setVolume(42);
	BOOST_CHECK_EQUAL(getVolume(), 42);
	setVolume(100);
	BOOST_CHECK_EQUAL(getVolume(), 100);
}

BOOST_FIXTURE_TEST_CASE(setvolume_invalid_value_fails, Fixture)
{
	BOOST_CHECK_THROW(setVolume(-1), range_error);
	BOOST_CHECK_THROW(setVolume(101), range_error);
}

// getTrackList is already tested in "initial_tracklist"
// getPlaylist is already tested in "initial_playlist"

BOOST_FIXTURE_TEST_CASE(addtoplaylist_adds_track, Fixture)
{
	Playlist expected = getPlaylist();
	expected.push_back(TRACK2);

	addToPlaylist(TRACK2.getFileName());
	BOOST_CHECK(getPlaylist() == expected);
}

BOOST_FIXTURE_TEST_CASE(addtoplaylist_no_such_track_fails, Fixture)
{
	BOOST_CHECK_THROW(addToPlaylist("foo"), range_error);
}

BOOST_FIXTURE_TEST_CASE(removefromplaylist_no_such_track_fails, Fixture)
{
	BOOST_CHECK_THROW(removeFromPlaylist(-1), range_error);
	BOOST_CHECK_THROW(removeFromPlaylist(2), range_error);
}

BOOST_FIXTURE_TEST_CASE(removefromplaylist_removes_track, Fixture)
{
	Playlist expected = getPlaylist();
	expected.erase(expected.begin());
	removeFromPlaylist(0);
	BOOST_CHECK(getPlaylist() == expected);
}

BOOST_FIXTURE_TEST_CASE(clearplaylist_clears_playlist, Fixture)
{
	clearPlaylist();
	BOOST_CHECK(getPlaylist().empty());
}

// Signals
BOOST_FIXTURE_TEST_CASE(play_track_fires_playback_status_changed_signal, Fixture)
{
	play(0);
	BOOST_CHECK(signalScheduled(sigPlaybackStatusChanged));
}

BOOST_FIXTURE_TEST_CASE(play_fires_playback_status_changed_signal, Fixture)
{
	play(0);
	stop();
	play();
	BOOST_CHECK(signalScheduled(sigPlaybackStatusChanged));
}

BOOST_FIXTURE_TEST_CASE(pause_fires_playback_status_changed_signal, Fixture)
{
	play(0);
	pause();
	BOOST_CHECK(signalScheduled(sigPlaybackStatusChanged));
}

BOOST_FIXTURE_TEST_CASE(playpause_fires_playback_status_changed_signal, Fixture)
{
	play(0);
	playPause();
	BOOST_CHECK(signalScheduled(sigPlaybackStatusChanged));
	playPause();
	BOOST_CHECK(signalScheduled(sigPlaybackStatusChanged));
}

BOOST_FIXTURE_TEST_CASE(stop_fires_playback_status_changed_signal, Fixture)
{
	play(0);
	stop();
	BOOST_CHECK(signalScheduled(sigPlaybackStatusChanged));
}

BOOST_FIXTURE_TEST_CASE(stop_stopped_does_not_fire_playback_status_changed_signal, Fixture)
{
	stop();
	BOOST_CHECK(!signalScheduled(sigPlaybackStatusChanged));
}

BOOST_FIXTURE_TEST_CASE(nexttrack_fires_playback_status_changed_signal, Fixture)
{
	play(0);
	nextTrack();
	BOOST_CHECK(signalScheduled(sigPlaybackStatusChanged));
}

BOOST_FIXTURE_TEST_CASE(prevtrack_fires_playback_status_changed_signal, Fixture)
{
	play(1);
	prevTrack();
	BOOST_CHECK(signalScheduled(sigPlaybackStatusChanged));
}

BOOST_FIXTURE_TEST_CASE(seek_fires_playback_status_changed_signal, Fixture)
{
	play(0);
	seek(boost::posix_time::seconds(5));
	BOOST_CHECK(signalScheduled(sigPlaybackStatusChanged));
}

BOOST_FIXTURE_TEST_CASE(addtoplaylist_fires_playlist_changed_signal, Fixture)
{
	addToPlaylist(TRACK1.getFileName());
	BOOST_CHECK(signalScheduled(sigPlaylistChanged));
}

BOOST_FIXTURE_TEST_CASE(removefromplaylist_fires_playlist_changed_signal, Fixture)
{
	removeFromPlaylist(0);
	BOOST_CHECK(signalScheduled(sigPlaylistChanged));
}

BOOST_FIXTURE_TEST_CASE(clearplaylist_fires_playlist_changed_signal, Fixture)
{
	clearPlaylist();
	BOOST_CHECK(signalScheduled(sigPlaylistChanged));
}

BOOST_FIXTURE_TEST_CASE(setvolume_fires_volume_changed_signal, Fixture)
{
	setVolume(50);
	BOOST_CHECK(signalScheduled(sigVolumeChanged));
}
